﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Iveonik.ConsoleWrapper
{
    /// <summary>
    /// Holder for command line parameters. Command line should consist from pairs {name} and {value}.
    /// The name of parameter is the string started after leading minis or slash end finished at the 
    /// first of "=", ":" or the end of parameter (what will reached first). If parameter isn't started 
    /// with "/", "-" or "--" it don't has a name (unnamed parameter, name == ""). Also can be absent a value.
    /// Names is case insensitive.
    /// 
    /// <code>
    /// "-name:value" = "name", ["value"];
    /// "--name:value" = "name", ["value"];
    ///	"/name=value" = "name", ["value"];
    ///	"/NoValued" = "novalued", [""];
    ///	"nonamed" = "", ["nonamed"];
    ///	"/p:1 /p:2" = "p", ["1", "2"];
    /// </code>
    /// </summary>
    public class Parameters
    {
        private readonly Dictionary<string, List<string>> _foundParams = new Dictionary<string, List<string>>();
        private readonly List<string> _names = new List<string>();

        /// <summary>
        /// Constructor. It starts process of the command line parsing.
        /// </summary>
        public Parameters():this(Environment.GetCommandLineArgs())
        {            
        }

        /// <summary>
        /// Constructor for unit-tests proposes.
        /// </summary>
        /// <param name="args">Imitation of or real Environment.GetCommandLineArgs().</param>
        protected Parameters(string[] args)
        {
            ParseCommandLine(args);
        }

        /// <summary>
        /// Unit-test initializer.
        /// </summary>
        /// <param name="args">Imitation of Environment.GetCommandLineArgs().</param>
        /// <returns>New instance.</returns>
        protected static Parameters TestInit(string[] args)
        {
            return new Parameters(args);
        }

        /// <summary>
        /// Method helps with checking occurence of some key in command line. Case insensitive.
        /// </summary>
        /// <param name="paramName">Parameter name. "" for unnamed parameters.</param>
        /// <returns>Returns true if command line has a parameter with passed name.</returns>
        public bool HasParameter(string paramName)
        {
            return _foundParams.ContainsKey(NormalizeName(paramName));
        }

        private static string NormalizeName(string paramName)
        {
            return paramName.ToUpperInvariant();
        }

        /// <summary>
        /// Method returns list of values for the passed parameter name.
        /// Names is case insensitive.
        /// </summary>
        /// <param name="paramName">Parameter name.</param>
        /// <returns>Values as list of strings.</returns>
        public IList<string> GetValues(string paramName){
            return _foundParams[NormalizeName(paramName)];
        }

        /// <summary>
        /// Method returns value for passed parameter and checks that this parameter entered once only.
        /// </summary>
        /// <param name="paramName">Parameter name.</param>
        /// <returns>Value of parameter.</returns>
        public string GetSingleValue(string paramName)
        {
            IList<string> values = GetValues(paramName);
            if (values.Count > 1)
                throw new InvalidOperationException(
                    String.Format("There are more than one parameter named '{0}'", paramName));

            return values.First();
        }

        /// <summary>
        /// Method returns list of names for found keys. Values can 
        /// be given with <see cref="GetValues"/> method.
        /// </summary>
        /// <returns>Names as list of strings.</returns>
        public IList<string> GetAllNames()
        {
            return _names.ToList();
        }

        private static readonly string[] HelpKeys = { "?", "h", "help" };
        /// <summary>
        /// Method checks found parameters and list. If unexpected parameters found, <see cref="ExitCodeException"/>
        /// will be thrown with code <see cref="ExitCodes.InadmissibleParameter"/>.
        /// </summary>
        /// <param name="admissibleParameters">List of admissable parameters.</param>
        /// <param name="admissHelpKeys">Add the help keys in the list of expectations.</param>
        public void CheckForUnexpectedParameters(IEnumerable<string> admissibleParameters, bool admissHelpKeys = true)
        {
            if (admissibleParameters == null)
                throw new ArgumentNullException("admissibleParameters");
            List<string> keysToCheck = admissibleParameters.ToList();
            if (admissHelpKeys)
                keysToCheck.AddRange(HelpKeys);
            Dictionary<string, string> expectedDic = new Dictionary<string,string>();
            foreach (string key in keysToCheck)
            {
                string keyName = NormalizeName(key);
                if (!expectedDic.ContainsKey(keyName))
                    expectedDic.Add(keyName, null);
            }
            foreach (string existedParamName in GetAllNames())
            {
                if (!expectedDic.ContainsKey(existedParamName))
                    throw new ExitCodeException(ExitCodes.InadmissibleParameter,
                        ConstructErrorMessage(existedParamName));
            }
        }

        private static string ConstructErrorMessage(string existedParamName)
        {
            StringBuilder sb = new StringBuilder();
            if (existedParamName == "")
                sb.AppendLine("Unexpected parameter name detected - '' (nonamed)");
            else
                sb.AppendLine("Unexpected parameter name detected - '" + existedParamName + "'");
            sb.AppendLine("For getting list of valid parameters, please run this module with /? command-line parameter.");
            return "" + sb;
        }

        /// <summary>
        /// Method checks found parameters to absence of mutually exclusive combinations. If illegal 
        /// combination found, <see cref="ExitCodeException"/>
        /// will be thrown with code <see cref="ExitCodes.InadmissibleParameter"/>.
        /// </summary>
        /// <param name="mutuallyExclusiveParameters">Set of rules.</param>
        public void CheckForMutuallyExclusiveParameters(IEnumerable<IEnumerable<string>> mutuallyExclusiveParameters)
        {
            if (mutuallyExclusiveParameters == null)
                return;
            foreach (var mutexParamsRec in mutuallyExclusiveParameters)
            {
                HashSet<string> alreadyProcessedParameters 
                    = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
                bool alreadyHas1parameter = false;
                foreach (string singleParam in mutexParamsRec)
                {
                    if (alreadyProcessedParameters.Contains(singleParam))
                    {
                        throw new ArgumentException(
                            string.Format("Mutually exclusive parameters can not be repeated in the same rule: '{0}' in {1}.",
                                singleParam, MutuallyExclusiveRuleToString(mutexParamsRec)),
                            "mutuallyExclusiveParameters");
                    }
                    alreadyProcessedParameters.Add(singleParam);
                    if (HasParameter(singleParam))
                    {
                        if (!alreadyHas1parameter)
                        {
                            alreadyHas1parameter = true;
                        }
                        else
                        {
                            throw new ExitCodeException(ExitCodes.InadmissibleParameter,
                                ConstructMutuallyExclusiveErrorMessage(mutexParamsRec));
                        }
                    }
                }
            }
        }

        private string ConstructMutuallyExclusiveErrorMessage(IEnumerable<string> mutexRuleRec)
        {
            return string.Format("Mutually exclusive set of parameters detected. Rule: [{0}].", MutuallyExclusiveRuleToString(mutexRuleRec));
        }

        private static string MutuallyExclusiveRuleToString(IEnumerable<string> mutexRuleRec)
        {
            return String.Join(", ", mutexRuleRec.Select(s => "'" + s + "'").ToArray());
        }

        private static readonly Regex ParametersParser = new Regex(@"^(?:(?:--|-|/)(?<Name>[^:=]+))?(?:=|:)?(?<Val>.*)",
            RegexOptions.Compiled);

        private void ParseCommandLine(string[] args)
        {
            _foundParams.Clear();
            _names.Clear();
            if (args == null) return;

            // First element - name of run application
            for (int i = 1; i < args.Length; ++i)
            {
                string arg = args[i];
                if (string.IsNullOrEmpty(arg)) continue;
                Match m = ParametersParser.Match(arg);
                if (m.Success)
                {
                    string name = m.Groups["Name"].Value;
                    string val = m.Groups["Val"].Value;

                    name = NormalizeName(name);
                    if (!_foundParams.ContainsKey(name))
                    {
                        _foundParams.Add(name, new List<string>());
                        _names.Add(name);
                    }
                    if (val != null)
                        _foundParams[name].Add(val);
                }
            }
        }

        private static string[] trueValues = new string[] { "true", "yes", "t", "y", "1", "on" };
        private static string[] falseValues = new string[] { "false", "no", "f", "n", "0", "off" };

        /// <summary>
        /// Method tries to parse some parameter as logic value.<br/>
        /// </summary>
        /// <param name="boolParameterName">Parameter to be processed.</param>
        /// <param name="defaultValue">Value to be returned if parameter's value cannot be interpreted 
        /// as logical value or is empty.</param>
        /// <returns>Result of parsing or default value.</returns>
        public bool GetBooleanValue(string boolParameterName, bool defaultValue)
        {
            bool result = defaultValue;

            if (HasParameter(boolParameterName))
            {
                string value = GetSingleValue(boolParameterName);
                if (trueValues.Contains(value, StringComparer.InvariantCultureIgnoreCase))
                {
                    result = true;
                }
                else if (falseValues.Contains(value, StringComparer.InvariantCultureIgnoreCase))
                {
                    result = false;
                }
            }
            return result;
        }
    }
}
